﻿/*
 * 
 * Combined Viewer from Image Tools http://imagetools.codeplex.com and Drag Viewer from http://silverlightscroller.codeplex.com
 * */

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics.Contracts;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Threading;
using ComicTool.Drawing;
using ImageTools;
using ImageTools.Controls;
using ImageTools.Helpers;

namespace ComicTool
{
    [TemplatePart(Name = ScrollViewerElementPart, Type = typeof(ScrollViewer))]
    [TemplatePart(Name = ImagePart, Type = typeof(AnimatedImage))]
    public class ImageEditorContainerEx : Control
    {
        #region Constants

        /// <summary>
        /// Defines the name for the 'ScrollViewer' template part, which is used 
        /// to scroll the image when it is zoomed in.
        /// </summary>
        public const string ScrollViewerElementPart = "ScrollViewer";

        /// <summary>
        /// Defines the name for the image element template part.
        /// </summary>
        public const string ImagePart = "Image";

        #endregion

        #region Fields

        private AnimatedImage imageElement;
        private double scaleFactor;
        private ScrollViewer scrollViewer;
        private CompositeTransform drawingCanvasScale;

        #endregion

        #region Dependency Properties

        /// <summary>
        /// Identifies the <see cref="RequiredSelectionHeight"/> property.
        /// </summary>
        public static readonly DependencyProperty RequiredSelectionHeightProperty =
            DependencyProperty.Register("RequiredSelectionHeight", typeof(int), typeof(ImageEditorContainerEx), null);

        /// <summary>
        /// Identifies the <see cref="RequiredSelectionWidth"/> property.
        /// </summary>
        public static readonly DependencyProperty RequiredSelectionWidthProperty =
            DependencyProperty.Register("RequiredSelectionWidth", typeof(int), typeof(ImageEditorContainerEx), null);

        /// <summary>
        /// Identifies the <see cref="Source"/> dependency property. 
        /// </summary>
        public static readonly DependencyProperty SourceProperty =
            DependencyProperty.Register("Source", typeof(ExtendedImage), typeof(ImageEditorContainerEx),
                                        new PropertyMetadata(OnSourceChanged));

        /// <summary>
        /// Defines the <see cref="ScalingMode"/> dependency property.
        /// </summary>
        public static readonly DependencyProperty ScalingModeProperty =
            DependencyProperty.Register("ScalingMode", typeof(ImageEditorScalingMode), typeof(ImageEditorContainerEx),
                                        new PropertyMetadata(ImageEditorScalingMode.Auto, OnScalingModeChanged));

        /// <summary>
        /// Identifies the <see cref="Scaling"/> property.
        /// </summary>
        public static readonly DependencyProperty ScalingProperty =
            DependencyProperty.Register("Scaling", typeof(double), typeof(ImageEditorContainerEx),
                                        new PropertyMetadata(OnScalingChanged));

        /// <summary>
        /// Identifies the <see cref="MoveSelection"/> property.
        /// </summary>
        public static readonly DependencyProperty MoveSelectionProperty =
            DependencyProperty.Register("MoveSelection", typeof(bool), typeof(ImageEditorContainerEx), null);

        public static readonly DependencyProperty DrawingCanvasProperty =
            DependencyProperty.Register("DrawingCanvas", typeof(Canvas), typeof(ImageEditorContainerEx),
                                        new PropertyMetadata(null));

        public static readonly DependencyProperty CanvasItemsProperty =
            DependencyProperty.Register("CanvasItems", typeof(IEnumerable<DrawingItem>),
                                        typeof(ImageEditorContainerEx),
                                        new PropertyMetadata(CanvasItemsChanged));

        /// <summary>
        /// Gets or sets the height of the selection border.
        /// </summary>
        /// <value>The height of the selection.</value>
        public int RequiredSelectionHeight
        {
            [ContractVerification(false)]
            get { return (int)GetValue(RequiredSelectionHeightProperty); }
            set { SetValue(RequiredSelectionHeightProperty, value); }
        }

        /// <summary>
        /// Gets or sets the width of the selection border.
        /// </summary>
        /// <value>The width of the selection.</value>
        public int RequiredSelectionWidth
        {
            [ContractVerification(false)]
            get { return (int)GetValue(RequiredSelectionWidthProperty); }
            set { SetValue(RequiredSelectionWidthProperty, value); }
        }

        /// <summary>
        /// Gets or sets the source for the image.
        /// </summary>
        public ExtendedImage Source
        {
            [ContractVerification(false)]
            get { return (ExtendedImage)GetValue(SourceProperty); }
            set { SetValue(SourceProperty, value); }
        }

        /// <summary>
        /// Gets or sets the selection mode.
        /// </summary>
        /// <value>The selection mode.</value>
        public ImageEditorScalingMode ScalingMode
        {
            [ContractVerification(false)]
            get { return (ImageEditorScalingMode)GetValue(ScalingModeProperty); }
            set { SetValue(ScalingModeProperty, value); }
        }

        /// <summary>
        /// Gets or sets the scaling, which defines how the image should be scaled.
        /// </summary>
        /// <value>The scaling.</value>
        /// <remarks>Define the zomming factor with a positive value or apply a negative value, if the image should
        /// be scaled to fit to the screeen.</remarks>
        public double Scaling
        {
            [ContractVerification(false)]
            get { return (double)GetValue(ScalingProperty); }
            set { SetValue(ScalingProperty, value); }
        }

        /// <summary>
        /// Gets or sets the interaction mode, which defines how the control behaves when the mouse is moved.
        /// </summary>
        /// <value>The interaction mode.</value>
        public bool MoveSelection
        {
            [ContractVerification(false)]
            get { return (bool)GetValue(MoveSelectionProperty); }
            set { SetValue(MoveSelectionProperty, value); }
        }

        public Canvas DrawingCanvas
        {
            get { return (Canvas)GetValue(DrawingCanvasProperty); }
            set { SetValue(DrawingCanvasProperty, value); }
        }

        public IEnumerable<DrawingItem> CanvasItems
        {
            get { return (IEnumerable<DrawingItem>)GetValue(CanvasItemsProperty); }
            set { SetValue(CanvasItemsProperty, value); }
        }

        private static void OnSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var owner = d as ImageEditorContainerEx;
            if (owner != null)
            {
                owner.OnSourceChanged();
            }
        }

        private void OnSourceChanged()
        {
            if (Source != null)
            {
                if (!Source.IsFilled)
                {
                    Source.LoadingCompleted += Source_LoadingCompleted;
                }
                else
                {
                    ScaleImage();
                }
            }
        }

        private void Source_LoadingCompleted(object sender, EventArgs e)
        {
            Dispatcher.BeginInvoke(() => OnSourceChanged());
        }

        private static void OnScalingModeChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
        {
            var owner = o as ImageEditorContainerEx;
            if (owner != null)
            {
                owner.ScaleImage();
            }
        }

        private static void OnScalingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var owner = d as ImageEditorContainerEx;
            if (owner != null)
            {
                owner.OnScalingChanged();
            }
        }

        private void OnScalingChanged()
        {
            Guard.GreaterThan(Scaling, 0, "Scaling", "Scaling cannot be less or equals than zero.");

            ScaleImage();
        }

        private static void CanvasItemsChanged(DependencyObject dObj, DependencyPropertyChangedEventArgs args)
        {
            var image = dObj as ImageEditorContainerEx;
            image.ScaleImage();
        }

        #endregion

        #region Constructors

        /// <summary>
        /// Initializes a new instance of the <see cref="ImageEditorContainerEx"/> class.
        /// </summary>
        public ImageEditorContainerEx()
        {
            DefaultStyleKey = typeof(ImageEditorContainerEx);

            SizeChanged += ImageEditorContainer_SizeChanged;
        }

        #endregion

        #region Methods

        #region Template Handling

        /// <summary>
        /// When overridden in a derived class, is invoked whenever application code or 
        /// internal processes (such as a rebuilding layout pass) 
        /// call <see cref="M:System.Windows.Controls.Control.ApplyTemplate"/>.
        /// </summary>
        public override void OnApplyTemplate()
        {
            BindXamlElements();

            if (!DesignerProperties.IsInDesignTool)
            {
                ScaleImage();
            }

            base.OnApplyTemplate();
        }

        private void BindXamlElements()
        {
            imageElement = GetTemplateChild(ImagePart) as AnimatedImage;
            DrawingCanvas = GetTemplateChild("DrawingCanvas") as Canvas;
            drawingCanvasScale = GetTemplateChild("DrawingCanvasScale") as CompositeTransform;

            scrollViewer = GetTemplateChild(ScrollViewerElementPart) as ScrollViewer;
            ShortcutKeyManager.Instance.AddListener(this);
            ShortcutKeyManager.Instance.AddListener(scrollViewer);
            ShortcutKeyManager.Instance.AddListener(imageElement);

            scrollViewer.MouseLeave += WrappedScrollViewer_MouseLeave;
            scrollViewer.MouseLeftButtonDown += WrappedScrollViewer_MouseLeftButtonDown;

            WrapScrollViewerContents();

            scrollViewer.MouseMove += WrappedScrollViewer_MouseMove;
            scrollViewer.MouseLeftButtonUp += WrappedScrollViewer_MouseLeftButtonUp;
        }

        #endregion

        #region Dragging

        private const double DEFAULT_FRICTION = 0.2;
        private const double MINIMUM_FRICTION = 0.0;
        private const double MAXIMUM_FRICTION = 1.0;
        private const double DRAG_POLLING_INTERVAL = 10; // milliseconds

        private Point _currentPoint;
        private DispatcherTimer _dragScrollTimer;
        private double _friction = DEFAULT_FRICTION;

        private bool _isDragging;
        private bool _mouseDown;
        private Point _previousPoint;
        private Point _previousPreviousPoint;

        public double Friction
        {
            get { return _friction; }
            set { _friction = Math.Min(Math.Max(value, MINIMUM_FRICTION), MAXIMUM_FRICTION); }
        }

        private Vector Momentum { get; set; }

        private Vector Velocity
        {
            get { return new Vector(_currentPoint.X - _previousPoint.X, _currentPoint.Y - _previousPoint.Y); }
        }

        // Using PreviousVelocity gives a smoother, better feeling as it leaves out any last frame momentum changes
        private Vector PreviousVelocity
        {
            get
            {
                return new Vector(_previousPoint.X - _previousPreviousPoint.X,
                                  _previousPoint.Y - _previousPreviousPoint.Y);
            }
        }

        private void WrapScrollViewerContents()
        {
            var content = scrollViewer.Content as UIElement;
            if (content != null)
            {
                content.MouseLeftButtonDown += WrappedScrollViewer_MouseLeftButtonDown;
            }
        }

        private void WrappedScrollViewer_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
        {
            CancelDrag(PreviousVelocity);
        }

        private void WrappedScrollViewer_MouseMove(object sender, MouseEventArgs e)
        {
            _currentPoint = e.GetPosition(scrollViewer);
            if (_mouseDown && !_isDragging)
            {
                _isDragging = true;
                DragScroll();
            }
        }

        private void WrappedScrollViewer_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            _currentPoint = _previousPoint = _previousPreviousPoint = e.GetPosition(scrollViewer);
            Momentum = new Vector(0, 0);
            BeginDrag();
        }

        private void WrappedScrollViewer_MouseLeave(object sender, MouseEventArgs e)
        {
            CancelDrag(PreviousVelocity);
        }

        private void BeginDrag()
        {
            _mouseDown = true;
            scrollViewer.Cursor = Cursors.Hand;
        }

        private void CancelDrag(Vector velocityToUse)
        {
            if (_isDragging)
            {
                Momentum = velocityToUse;
            }
            _isDragging = false;
            _mouseDown = false;
            scrollViewer.Cursor = Cursors.Arrow;
        }

        protected void DragScroll()
        {
            if (_dragScrollTimer == null)
            {
                _dragScrollTimer = new DispatcherTimer();
                _dragScrollTimer.Tick += TickDragScroll;
                _dragScrollTimer.Interval = new TimeSpan(0, 0, 0, 0, (int)DRAG_POLLING_INTERVAL);
                _dragScrollTimer.Start();
            }
        }

        private void TickDragScroll(object sender, EventArgs e)
        {
            if (_isDragging)
            {
                GeneralTransform generalTransform = scrollViewer.TransformToVisual(scrollViewer);
                Point childToParentCoordinates = generalTransform.Transform(new Point(0, 0));
                var bounds = new Rect(childToParentCoordinates, scrollViewer.RenderSize);

                if (bounds.Contains(_currentPoint))
                {
                    PerformScroll(PreviousVelocity);
                }

                if (!_mouseDown)
                {
                    CancelDrag(Velocity);
                }
                _previousPreviousPoint = _previousPoint;
                _previousPoint = _currentPoint;
            }
            else if (Momentum.Length > 0)
            {
                Momentum *= (1.0 - _friction / 4.0);
                PerformScroll(Momentum);
            }
            else
            {
                if (_dragScrollTimer != null)
                {
                    _dragScrollTimer.Tick -= TickDragScroll;
                    _dragScrollTimer.Stop();
                    _dragScrollTimer = null;
                }
            }
        }

        private void CancelDrag()
        {
            _isDragging = false;
            Momentum = Velocity;
        }

        private void PerformScroll(Vector displacement)
        {
            double verticalOffset = Math.Max(0.0, scrollViewer.VerticalOffset - displacement.Y);
            scrollViewer.ScrollToVerticalOffset(verticalOffset);

            double horizontalOffset = Math.Max(0.0, scrollViewer.HorizontalOffset - displacement.X);
            scrollViewer.ScrollToHorizontalOffset(horizontalOffset);
        }

        private class Vector
        {
            public Vector(double x, double y)
            {
                X = x;
                Y = y;
            }

            public double Length
            {
                get { return Math.Sqrt(X * X + Y * Y); }
            }

            public double X { get; set; }
            public double Y { get; set; }

            public static Vector operator *(Vector vector, double scalar)
            {
                return new Vector(vector.X * scalar, vector.Y * scalar);
            }
        }

        #endregion

        public ScrollViewer Viewer
        {
            get { return scrollViewer; }
        }

        private void ImageEditorContainer_SizeChanged(object sender, SizeChangedEventArgs e)
        {
            ScaleImage();
        }

        private void ScaleImage()
        {
            if (scrollViewer != null && scrollViewer.ViewportHeight > 0 && imageElement != null)
            {
                if (ScalingMode == ImageEditorScalingMode.Auto)
                {
                    if (Source != null && Source.IsFilled)
                    {
                        double newWidth = 0;

                        double viewportRatio = scrollViewer.ViewportWidth / scrollViewer.ViewportHeight;

                        if (viewportRatio > Source.PixelRatio)
                        {
                            newWidth = Math.Max(0, scrollViewer.ViewportHeight - 20) * Source.PixelRatio;
                        }
                        else
                        {
                            newWidth = Math.Max(0, scrollViewer.ViewportWidth - 20);
                        }

                        Scaling = newWidth / Source.PixelWidth;
                    }
                }

                if (Scaling > 0)
                {
                    // double oldScaling = scaleFactor;

                    scaleFactor = Scaling;

                    if (Source != null)
                    {
                        imageElement.Width = Source.PixelWidth * Scaling;
                        imageElement.Height = Source.PixelHeight * Scaling;
                        drawingCanvasScale.ScaleX = Scaling;
                        drawingCanvasScale.ScaleY = Scaling;
                        if (CanvasItems != null)
                        {
                            CanvasItems.ForEach(p => p.Draw(DrawingCanvas, Source.PixelWidth, Source.PixelHeight));
                        }
                    }

                    //if (scaleFactor > 0 && scaleFactor.IsNumber())
                    //{
                    //    double dScale = oldScaling == 0 || !oldScaling.IsNumber()
                    //                        ? scaleFactor
                    //                        : scaleFactor / oldScaling;
                    //}
                }
            }
        }

        #endregion
    }
}